#include "isl_interface.h"

#include "main.h"
#include "server_logger.h"

#include <QSslSocket>
#include <google/protobuf/descriptor.h>
#include <libcockatrice/protocol/debug_pb_message.h>
#include <libcockatrice/protocol/get_pb_extension.h>
#include <libcockatrice/protocol/pb/event_game_joined.pb.h>
#include <libcockatrice/protocol/pb/event_join_room.pb.h>
#include <libcockatrice/protocol/pb/event_leave_room.pb.h>
#include <libcockatrice/protocol/pb/event_list_games.pb.h>
#include <libcockatrice/protocol/pb/event_remove_messages.pb.h>
#include <libcockatrice/protocol/pb/event_room_say.pb.h>
#include <libcockatrice/protocol/pb/event_server_complete_list.pb.h>
#include <libcockatrice/protocol/pb/event_user_joined.pb.h>
#include <libcockatrice/protocol/pb/event_user_left.pb.h>
#include <libcockatrice/protocol/pb/event_user_message.pb.h>
#include <libcockatrice/protocol/pb/isl_message.pb.h>
#include <server_protocolhandler.h>
#include <server_room.h>

void IslInterface::sharedCtor(const QSslCertificate &cert, const QSslKey &privateKey)
{
    socket = new QSslSocket(this);
    socket->setLocalCertificate(cert);
    socket->setPrivateKey(privateKey);

    connect(socket, SIGNAL(readyRead()), this, SLOT(readClient()), Qt::QueuedConnection);
    connect(socket, SIGNAL(error(QAbstractSocket::SocketError)), this,
            SLOT(catchSocketError(QAbstractSocket::SocketError)));
    connect(this, SIGNAL(outputBufferChanged()), this, SLOT(flushOutputBuffer()), Qt::QueuedConnection);
}

IslInterface::IslInterface(int _socketDescriptor,
                           const QSslCertificate &cert,
                           const QSslKey &privateKey,
                           Servatrice *_server)
    : QObject(), socketDescriptor(_socketDescriptor), server(_server), messageInProgress(false)
{
    sharedCtor(cert, privateKey);
}

IslInterface::IslInterface(int _serverId,
                           const QString &_peerHostName,
                           const QString &_peerAddress,
                           int _peerPort,
                           const QSslCertificate &_peerCert,
                           const QSslCertificate &cert,
                           const QSslKey &privateKey,
                           Servatrice *_server)
    : QObject(), serverId(_serverId), peerHostName(_peerHostName), peerAddress(_peerAddress), peerPort(_peerPort),
      peerCert(_peerCert), server(_server), messageInProgress(false)
{
    sharedCtor(cert, privateKey);
}

IslInterface::~IslInterface()
{
    logger->logMessage("[ISL] session ended", this);

    flushOutputBuffer();

    // As these signals are connected with Qt::QueuedConnection implicitly,
    // we don't need to worry about them modifying the lists while we're iterating.

    server->roomsLock.lockForRead();
    QMapIterator<int, Server_Room *> roomIterator(server->getRooms());
    while (roomIterator.hasNext()) {
        Server_Room *room = roomIterator.next().value();
        room->usersLock.lockForRead();
        QMapIterator<QString, ServerInfo_User_Container> roomUsers(room->getExternalUsers());
        while (roomUsers.hasNext()) {
            roomUsers.next();
            if (roomUsers.value().getUserInfo()->server_id() == serverId)
                emit externalRoomUserLeft(room->getId(), roomUsers.key());
        }
        room->usersLock.unlock();
    }
    server->roomsLock.unlock();

    server->clientsLock.lockForRead();
    QMapIterator<QString, Server_AbstractUserInterface *> extUsers(server->getExternalUsers());
    while (extUsers.hasNext()) {
        extUsers.next();
        if (extUsers.value()->getUserInfo()->server_id() == serverId)
            emit externalUserLeft(extUsers.key());
    }
    server->clientsLock.unlock();
}

void IslInterface::initServer()
{
    socket->setSocketDescriptor(socketDescriptor);

    logger->logMessage(QString("[ISL] incoming connection: %1").arg(socket->peerAddress().toString()));

    QList<ServerProperties> serverList = server->getServerList();
    int listIndex = -1;
    for (int i = 0; i < serverList.size(); ++i)
        if (serverList[i].address == socket->peerAddress()) {
            listIndex = i;
            break;
        }
    if (listIndex == -1) {
        logger->logMessage(
            QString("[ISL] address %1 unknown, terminating connection").arg(socket->peerAddress().toString()));
        deleteLater();
        return;
    }

    socket->startServerEncryption();
    if (!socket->waitForEncrypted(5000)) {
#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0))
        QList<QSslError> sslErrors(socket->sslHandshakeErrors());
#else
        QList<QSslError> sslErrors(socket->sslErrors());
#endif
        if (sslErrors.isEmpty())
            qDebug() << "[ISL] SSL handshake timeout, terminating connection";
        else
            qDebug() << "[ISL] SSL errors:" << sslErrors;
        deleteLater();
        return;
    }

    if (serverList[listIndex].cert == socket->peerCertificate())
        logger->logMessage(QString("[ISL] Peer authenticated as " + serverList[listIndex].hostname));
    else {
        logger->logMessage(QString("[ISL] Authentication failed, terminating connection"));
        deleteLater();
        return;
    }
    serverId = serverList[listIndex].id;

    Event_ServerCompleteList event;
    event.set_server_id(server->getServerID());

    server->clientsLock.lockForRead();
    QMapIterator<QString, Server_ProtocolHandler *> userIterator(server->getUsers());
    while (userIterator.hasNext())
        event.add_user_list()->CopyFrom(userIterator.next().value()->copyUserInfo(true, true));
    server->clientsLock.unlock();

    server->roomsLock.lockForRead();
    QMapIterator<int, Server_Room *> roomIterator(server->getRooms());
    while (roomIterator.hasNext()) {
        Server_Room *room = roomIterator.next().value();
        room->usersLock.lockForRead();
        room->gamesLock.lockForRead();
        room->getInfo(*event.add_room_list(), true, true, false);
    }

    IslMessage message;
    message.set_message_type(IslMessage::SESSION_EVENT);
    SessionEvent *sessionEvent = message.mutable_session_event();
    sessionEvent->GetReflection()
        ->MutableMessage(sessionEvent, event.GetDescriptor()->FindExtensionByName("ext"))
        ->CopyFrom(event);

    server->islLock.lockForWrite();
    if (server->islConnectionExists(serverId)) {
        qDebug() << "[ISL] Duplicate connection to #" << serverId << "terminating connection";
        deleteLater();
    } else {
        transmitMessage(message);
        server->addIslInterface(serverId, this);
    }
    server->islLock.unlock();

    roomIterator.toFront();
    while (roomIterator.hasNext()) {
        roomIterator.next();
        roomIterator.value()->gamesLock.unlock();
        roomIterator.value()->usersLock.unlock();
    }
    server->roomsLock.unlock();
}

void IslInterface::initClient()
{
    QList<QSslError> expectedErrors;
    expectedErrors.append(QSslError(QSslError::SelfSignedCertificate, peerCert));
    socket->ignoreSslErrors(expectedErrors);

    qDebug() << "[ISL] Connecting to #" << serverId << ":" << peerAddress << ":" << peerPort;

    socket->connectToHostEncrypted(peerAddress, peerPort, peerHostName);
    if (!socket->waitForConnected(5000)) {
        qDebug() << "[ISL] Socket error:" << socket->errorString();
        deleteLater();
        return;
    }
    if (!socket->waitForEncrypted(5000)) {
#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0))
        QList<QSslError> sslErrors(socket->sslHandshakeErrors());
#else
        QList<QSslError> sslErrors(socket->sslErrors());
#endif
        if (sslErrors.isEmpty())
            qDebug() << "[ISL] SSL handshake timeout, terminating connection";
        else
            qDebug() << "[ISL] SSL errors:" << sslErrors;
        deleteLater();
        return;
    }

    server->islLock.lockForWrite();
    if (server->islConnectionExists(serverId)) {
        qDebug() << "[ISL] Duplicate connection to #" << serverId << "terminating connection";
        deleteLater();
        return;
    }

    server->addIslInterface(serverId, this);
    server->islLock.unlock();
}

void IslInterface::flushOutputBuffer()
{
    QMutexLocker locker(&outputBufferMutex);
    if (outputBuffer.isEmpty())
        return;
    server->incTxBytes(outputBuffer.size());
    socket->write(outputBuffer);
    socket->flush();
    outputBuffer.clear();
}

void IslInterface::readClient()
{
    QByteArray data = socket->readAll();
    server->incRxBytes(data.size());
    inputBuffer.append(data);

    do {
        if (!messageInProgress) {
            if (inputBuffer.size() >= 4) {
                messageLength = (((quint32)(unsigned char)inputBuffer[0]) << 24) +
                                (((quint32)(unsigned char)inputBuffer[1]) << 16) +
                                (((quint32)(unsigned char)inputBuffer[2]) << 8) +
                                ((quint32)(unsigned char)inputBuffer[3]);
                inputBuffer.remove(0, 4);
                messageInProgress = true;
            } else
                return;
        }
        if (inputBuffer.size() < messageLength)
            return;

        IslMessage newMessage;
        newMessage.ParseFromArray(inputBuffer.data(), messageLength);
        inputBuffer.remove(0, messageLength);
        messageInProgress = false;

        processMessage(newMessage);
    } while (!inputBuffer.isEmpty());
}

void IslInterface::catchSocketError(QAbstractSocket::SocketError socketError)
{
    qDebug() << "[ISL] Socket error:" << socketError;

    server->islLock.lockForWrite();
    server->removeIslInterface(serverId);
    server->islLock.unlock();

    deleteLater();
}

void IslInterface::transmitMessage(const IslMessage &item)
{
    QByteArray buf;
#if GOOGLE_PROTOBUF_VERSION > 3001000
    unsigned int size = static_cast<unsigned int>(item.ByteSizeLong());
#else
    unsigned int size = static_cast<unsigned int>(item.ByteSize());
#endif
    buf.resize(size + 4);
    item.SerializeToArray(buf.data() + 4, size);
    buf.data()[3] = (unsigned char)size;
    buf.data()[2] = (unsigned char)(size >> 8);
    buf.data()[1] = (unsigned char)(size >> 16);
    buf.data()[0] = (unsigned char)(size >> 24);

    outputBufferMutex.lock();
    outputBuffer.append(buf);
    outputBufferMutex.unlock();
    emit outputBufferChanged();
}

void IslInterface::sessionEvent_ServerCompleteList(const Event_ServerCompleteList &event)
{
    for (int i = 0; i < event.user_list_size(); ++i) {
        ServerInfo_User temp(event.user_list(i));
        temp.set_server_id(serverId);
        emit externalUserJoined(temp);
    }
    for (int i = 0; i < event.room_list_size(); ++i) {
        const ServerInfo_Room &room = event.room_list(i);
        for (int j = 0; j < room.user_list_size(); ++j) {
            ServerInfo_User userInfo(room.user_list(j));
            userInfo.set_server_id(serverId);
            emit externalRoomUserJoined(room.room_id(), userInfo);
        }
        for (int j = 0; j < room.game_list_size(); ++j) {
            ServerInfo_Game gameInfo(room.game_list(j));
            gameInfo.set_server_id(serverId);
            emit externalRoomGameListChanged(room.room_id(), gameInfo);
        }
    }
}

void IslInterface::sessionEvent_UserJoined(const Event_UserJoined &event)
{
    ServerInfo_User userInfo(event.user_info());
    userInfo.set_server_id(serverId);
    emit externalUserJoined(userInfo);
}

void IslInterface::sessionEvent_UserLeft(const Event_UserLeft &event)
{
    emit externalUserLeft(QString::fromStdString(event.name()));
}

void IslInterface::roomEvent_UserJoined(int roomId, const Event_JoinRoom &event)
{
    ServerInfo_User userInfo(event.user_info());
    userInfo.set_server_id(serverId);
    emit externalRoomUserJoined(roomId, userInfo);
}

void IslInterface::roomEvent_UserLeft(int roomId, const Event_LeaveRoom &event)
{
    emit externalRoomUserLeft(roomId, QString::fromStdString(event.name()));
}

void IslInterface::roomEvent_Say(int roomId, const Event_RoomSay &event)
{
    emit externalRoomSay(roomId, QString::fromStdString(event.name()), QString::fromStdString(event.message()));
}

void IslInterface::roomEvent_ListGames(int roomId, const Event_ListGames &event)
{
    for (int i = 0; i < event.game_list_size(); ++i) {
        ServerInfo_Game gameInfo(event.game_list(i));
        gameInfo.set_server_id(serverId);
        emit externalRoomGameListChanged(roomId, gameInfo);
    }
}

void IslInterface::roomEvent_RemoveMessages(int roomId, const Event_RemoveMessages &event)
{
    emit externalRoomRemoveMessages(roomId, QString::fromStdString(event.name()), event.amount());
}

void IslInterface::roomCommand_JoinGame(const Command_JoinGame &cmd, int cmdId, int roomId, qint64 sessionId)
{
    emit joinGameCommandReceived(cmd, cmdId, roomId, serverId, sessionId);
}

void IslInterface::processSessionEvent(const SessionEvent &event, qint64 sessionId)
{
    switch (getPbExtension(event)) {
        case SessionEvent::SERVER_COMPLETE_LIST:
            sessionEvent_ServerCompleteList(event.GetExtension(Event_ServerCompleteList::ext));
            break;
        case SessionEvent::USER_JOINED:
            sessionEvent_UserJoined(event.GetExtension(Event_UserJoined::ext));
            break;
        case SessionEvent::USER_LEFT:
            sessionEvent_UserLeft(event.GetExtension(Event_UserLeft::ext));
            break;
        case SessionEvent::GAME_JOINED: {
            QReadLocker clientsLocker(&server->clientsLock);
            Server_AbstractUserInterface *client = server->getUsersBySessionId().value(sessionId);
            if (!client) {
                qDebug() << "IslInterface::processSessionEvent: session id" << sessionId << "not found";
                break;
            }
            const Event_GameJoined &gameJoined = event.GetExtension(Event_GameJoined::ext);
            client->playerAddedToGame(gameJoined.game_info().game_id(), gameJoined.game_info().room_id(),
                                      gameJoined.player_id());
            client->sendProtocolItem(event);
            break;
        }
        case SessionEvent::USER_MESSAGE:
        case SessionEvent::REPLAY_ADDED: {
            QReadLocker clientsLocker(&server->clientsLock);
            Server_AbstractUserInterface *client = server->getUsersBySessionId().value(sessionId);
            if (!client) {
                qDebug() << "IslInterface::processSessionEvent: session id" << sessionId << "not found";
                break;
            }

            client->sendProtocolItem(event);
            break;
        }
        default:;
    }
}

void IslInterface::processRoomEvent(const RoomEvent &event)
{
    switch (getPbExtension(event)) {
        case RoomEvent::JOIN_ROOM:
            roomEvent_UserJoined(event.room_id(), event.GetExtension(Event_JoinRoom::ext));
            break;
        case RoomEvent::LEAVE_ROOM:
            roomEvent_UserLeft(event.room_id(), event.GetExtension(Event_LeaveRoom::ext));
            break;
        case RoomEvent::ROOM_SAY:
            roomEvent_Say(event.room_id(), event.GetExtension(Event_RoomSay::ext));
            break;
        case RoomEvent::LIST_GAMES:
            roomEvent_ListGames(event.room_id(), event.GetExtension(Event_ListGames::ext));
            break;
        case RoomEvent::REMOVE_MESSAGES:
            roomEvent_RemoveMessages(event.room_id(), event.GetExtension(Event_RemoveMessages::ext));
            break;
        default:;
    }
}

void IslInterface::processRoomCommand(const CommandContainer &cont, qint64 sessionId)
{
    for (int i = 0; i < cont.room_command_size(); ++i) {
        const RoomCommand &roomCommand = cont.room_command(i);
        switch (static_cast<RoomCommand::RoomCommandType>(getPbExtension(roomCommand))) {
            case RoomCommand::JOIN_GAME:
                roomCommand_JoinGame(roomCommand.GetExtension(Command_JoinGame::ext), cont.cmd_id(), cont.room_id(),
                                     sessionId);
            default:;
        }
    }
}

void IslInterface::processMessage(const IslMessage &item)
{
    qDebug() << getSafeDebugString(item);

    switch (item.message_type()) {
        case IslMessage::ROOM_COMMAND_CONTAINER: {
            processRoomCommand(item.room_command(), item.session_id());
            break;
        }
        case IslMessage::GAME_COMMAND_CONTAINER: {
            emit gameCommandContainerReceived(item.game_command(), item.player_id(), serverId, item.session_id());
            break;
        }
        case IslMessage::SESSION_EVENT: {
            processSessionEvent(item.session_event(), item.session_id());
            break;
        }
        case IslMessage::RESPONSE: {
            emit responseReceived(item.response(), item.session_id());
            break;
        }
        case IslMessage::GAME_EVENT_CONTAINER: {
            emit gameEventContainerReceived(item.game_event_container(), item.session_id());
            break;
        }
        case IslMessage::ROOM_EVENT: {
            processRoomEvent(item.room_event());
            break;
        }
        default:;
    }
}
